Skip to content

Comments

feat(developer): convert .keyman-touch-layout into KMX+ data and embed 🔱#15613

Draft
mcdurdin wants to merge 1 commit intoepic/embed-osk-in-kmxfrom
feat/developer/embed-osk/convert-touch-layout-into-kmx-plus
Draft

feat(developer): convert .keyman-touch-layout into KMX+ data and embed 🔱#15613
mcdurdin wants to merge 1 commit intoepic/embed-osk-in-kmxfrom
feat/developer/embed-osk/convert-touch-layout-into-kmx-plus

Conversation

@mcdurdin
Copy link
Member

Basic conversion of .keyman-touch-layout into KMX+, handling merge with .kvks, structure, key caps, flicks, multitaps, and hints.

TODO: 'special' key types, padding/gaps, conversion of CAP graphical key caps; search TODO-EMBED-OSK-IN-KMX for more

Test-bot: skip

Basic conversion of .keyman-touch-layout into KMX+, handling merge with
.kvks, structure, key caps, flicks, multitaps, and hints.

TODO: 'special' key types, padding/gaps, conversion of *CAP* graphical
key caps; search TODO-EMBED-OSK-IN-KMX for more

Test-bot: skip
@keymanapp-test-bot
Copy link

keymanapp-test-bot bot commented Feb 23, 2026

User Test Results

Test specification and instructions

User tests are not required

Test Artifacts

  • Android
    • Keyman for Android apk - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for Android apk - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for Android apk (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • KeyboardHarness apk - build : all tests passed (no artifacts on BuildLevel "build")
    • Keyman for Android apk (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • KMSample1 apk - build : all tests passed (no artifacts on BuildLevel "build")
    • KMSample2 apk - build : all tests passed (no artifacts on BuildLevel "build")
  • Developer
    • Keyman Developer - build : all tests passed (no artifacts on BuildLevel "build")
    • Compiler Regression Tests - build : all tests passed (no artifacts on BuildLevel "build")
    • Keyman Developer (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • kmcomp.zip - build : all tests passed (no artifacts on BuildLevel "build")
    • kmcomp.zip (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
  • iOS
    • Keyman for iOS (simulator image) - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for iOS (simulator image) - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for iOS (simulator image) (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • Keyman for iOS (simulator image) (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
  • Keyboards
    • Test Keyboards - build : all tests passed (no artifacts on BuildLevel "build")
  • Web
    • KeymanWeb Test Home - build : all tests passed (no artifacts on BuildLevel "build")
  • Windows
    • Keyman for Windows - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for Windows - build : all tests passed (no artifacts on BuildLevel "build")
    • FirstVoices Keyboards for Windows (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • Keyman for Windows (old PRs) - build : all tests passed (no artifacts on BuildLevel "build")
    • Text Editor (ARM64) - build : all tests passed (no artifacts on BuildLevel "build")
    • Text Editor (x64) - build : all tests passed (no artifacts on BuildLevel "build")
    • Text Editor (x86) - build : all tests passed (no artifacts on BuildLevel "build")

@keymanapp-test-bot keymanapp-test-bot bot changed the title feat(developer): convert .keyman-touch-layout into KMX+ data and embed feat(developer): convert .keyman-touch-layout into KMX+ data and embed 🔱 Feb 23, 2026
@keymanapp-test-bot keymanapp-test-bot bot added this to the A19S23 milestone Feb 23, 2026
*
* .keyman-touch-layout JSON format definitions
*
* Follows scheams in /common/schemas/keyman-touch-layout/, using
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Follows scheams in /common/schemas/keyman-touch-layout/, using
* Follows schemas in /common/schemas/keyman-touch-layout/, using

// { "id": "K_F", "text": "blank", "sp": TouchLayout.TouchLayoutKeySp.blank, "width": 100, },
// { "id": "K_G", "text": "spacer", "sp": TouchLayout.TouchLayoutKeySp.spacer, "width": 100, },
// ];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need tests for loadTouchLayoutFile, especially for a non-existing, empty or invalid file?

}

private addKeyFromTouchLayoutKey(kmxplus: KMXPlus.DependencySections, er: KMXPlus.LayrRow, idPrefix: string, key: TouchLayout.TouchLayoutKey): void {
const lk = new KMXPlus.KeysKeys();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a better variable name than lk? keyskeys isn't ideal either but would at least have a connection to the type.

The same "problem" exists in other functions, but they are shorter so it's easier to see the definition of the variable.

}

private addRowFromTouchLayoutRow(kmxplus: KMXPlus.KMXPlusData, entry: KMXPlus.LayrEntry, idPrefix: string, row: TouchLayout.TouchLayoutRow): void {
const er = new KMXPlus.LayrRow();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const er = new KMXPlus.LayrRow();
const layrRow = new KMXPlus.LayrRow();

return kmxplus.strs.allocString('');
} else if(text.trim() === '' && id.startsWith('U_')) {
// if key cap == U_xxxx[_yyyy], then we generate key cap from that
return kmxplus.strs.allocString(this.unicodeKeyIdToString(id));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If id has an invalid U_ id this will result in an empty key cap and not produce a warning. I don't know if this can happen or if we know here that id always contains a valid U_ sequence. (Flagged by devin.ai)

KMXPlus.DispItemFlags.keyCap123, //TODO-EMBED-OSK-IN-KMX
}
kmxplus.disp.disps.push(disp);
return kmxplus.strs.allocString('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right that we return an empty string for *key* ?

const lk = new KMXPlus.KeysKeys();

lk.id = kmxplus.strs.allocString(idPrefix + key.id);
lk.to = this.getKeyCap(kmxplus, key.id, key.text); // kmxplus.strs.allocString(subKey.text);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

devin.ai identified this problem - I can't tell, but it looks like it might be a real problem. We should probably add a unit test.

DispItem.toId references unprefixed key ID for main keys with special text patterns

When addKeyFromTouchLayoutKey processes a main key whose text matches the *...* pattern (e.g. *Enter*), the getKeyCap method creates a DispItem with toId set to the unprefixed key ID, while the key itself is stored with a prefixed ID.

Root Cause

At embed-osk-touch-layout.ts:99, the key's actual id is set to the prefixed form:

lk.id = kmxplus.strs.allocString(idPrefix + key.id); // e.g. "phone-default-K_ENTER"

But at embed-osk-touch-layout.ts:100, getKeyCap is called with the unprefixed key.id:

lk.to = this.getKeyCap(kmxplus, key.id, key.text); // passes "K_ENTER"

Inside getKeyCap at embed-osk-touch-layout.ts:208, when the text matches *...*, the DispItem.toId is allocated from the unprefixed id:

toId: kmxplus.strs.allocString(id), // allocates "K_ENTER" instead of "phone-default-K_ENTER"

Contrast this with the hint DispItem at embed-osk-touch-layout.ts:141, which correctly uses the prefixed lk.id:

toId: lk.id, // correctly references "phone-default-K_Q"

For subkeys (processed via keyFromSubKey), this is not a problem because subkey IDs are not prefixed — subKey.id matches lk.id. But for main keys, the mismatch means the runtime will fail to look up the display for these keys because the DispItem.toId won't match any key's actual ID.

Impact: Any main key (not subkey) with a special text pattern like *Enter*, *Shift*, etc. will have a broken display mapping in the compiled KMX+ output. The runtime won't find the DispItem for these keys, causing incorrect rendering of special key caps.

Suggested change
lk.to = this.getKeyCap(kmxplus, key.id, key.text); // kmxplus.strs.allocString(subKey.text);
lk.to = this.getKeyCap(kmxplus, idPrefix + key.id, key.text); // kmxplus.strs.allocString(subKey.text);

kmxplus.keys.keys.push(lk);
flicks.flicks.push(flick);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another section that devin.ai flagged:

Subkey IDs are not deduplicated, risking duplicate KeysKeys entries

The keyFromSubKey method at embed-osk-touch-layout.ts:186-198 creates a new KeysKeys and pushes it unconditionally at lines 161 and 182. If the same subkey ID (e.g. K_1) is referenced by both a longpress (sk) and a flick on the same or different keys, duplicate KeysKeys entries with the same id will be added to kmxplus.keys.keys. Compare this with the LDML compiler in developer/src/kmc-ldml/src/compiler/keys.ts:314, which tracks used keys in a Set<string> to avoid duplicates. Depending on how the KMX+ builder serializes and the runtime looks up keys, this may cause issues or merely waste space. Worth investigating whether deduplication is needed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants